package cn.rdtimes.wolfdsp.core.invoker;

import cn.rdtimes.wolfdsp.core.util.StringUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 执行shell命令
 *
 * @author BZ
 */
class ShellCommand {
    // 监控执行超过时间, >0监控
    private int timeout;
    // 环境变量
    private Map<String, String> envs;
    // 工作路径
    private File workDir;
    // 编码
    private String encoding;
    // 完成标识
    private boolean completed;
    // 执行的指令
    private String[] commands;
    private CommandLineWatch lineWatch;
    private Process process;
    // shell进程退出值
    private int exitCode;

    public ShellCommand(int timeout, Map<String, String> envs, File workDir,
                        String encoding, String[] commands, CommandLineWatch lineWatch) {
        this.timeout = timeout;
        this.envs = envs;
        this.workDir = workDir;
        this.encoding = encoding;
        this.commands = commands;
        this.lineWatch = lineWatch;
    }

    StringBuilder execute() throws Exception {
        StringBuilder sb = new StringBuilder(128);
        String separator = System.getProperty("line.separator", "\n");

        Timer timeoutTimer = null;
        BufferedReader errReader = null;
        BufferedReader inputReader = null;
        Thread errThread = null;

        try {
            ProcessBuilder pb = new ProcessBuilder();
            if (envs != null && envs.size() > 0) {
                pb.environment().putAll(envs);
            }
            if (workDir != null) {
                pb.directory(workDir);
            }
            pb.command(commands);
            process = pb.start();


            // watch process timeout
            if (timeout > 0) {
                timeoutTimer = new Timer();
                timeoutTimer.schedule(new TimeoutTask(), timeout);
            }

            errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), encoding));
            inputReader = new BufferedReader(new InputStreamReader(process.getInputStream(), encoding));

            StringBuilder errsb = new StringBuilder(32);
            BufferedReader finalErrReader = errReader;
            errThread = new Thread() {
                @Override
                public void run() {
                    try {
                        String line = finalErrReader.readLine();
                        while ((line != null) && !isInterrupted()) {
                            errsb.append(line);
                            errsb.append(separator);
                            if (lineWatch != null) {
                                lineWatch.watchErrLine(line);
                            }
                            line = finalErrReader.readLine();
                        }
                    } catch (IOException e) {
                        // ignore
                    }
                }
            };
            try {
                errThread.start();
            } catch (Exception e) {
                // ignore
            }

            String line = inputReader.readLine();
            while (line != null) {
                sb.append(line);
                sb.append(separator);
                if (lineWatch != null) {
                    lineWatch.watchInputLine(line);
                }
                line = inputReader.readLine();
            }
            exitCode = process.waitFor();
            try {
                errThread.join();
            } catch (Exception e) {
                // ignore
            }
            // morge log
            sb.append(errsb);
            completed = true;
            return sb;
        } finally {
            if (timeoutTimer != null) {
                timeoutTimer.cancel();
            }
            if (!completed && errThread != null) {
                errThread.interrupt();
            }
            if (errReader != null) {
                try {
                    errReader.close();
                } catch (Exception e) {
                    // ignore
                }
            }
            if (inputReader != null) {
                try {
                    inputReader.close();
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    }

    int exitCode() {
        return exitCode;
    }

    int getTimeout() {
        return timeout;
    }

    Map<String, String> getEnvs() {
        return envs;
    }

    File getWorkDir() {
        return workDir;
    }

    boolean isCompleted() {
        return completed;
    }

    String[] getCommands() {
        return commands;
    }

    String getEncoding() {
        return encoding;
    }

    static ShellCommandBuilder builder() {
        return new ShellCommandBuilder();
    }

    private class TimeoutTask extends TimerTask {
        public void run() {
            try {
                ShellCommand.this.process.exitValue();
            } catch (Exception e) {
                // 应该超时, 需要主动销毁
                if (!ShellCommand.this.completed) {
                    ShellCommand.this.process.destroy();
                }
            }
        }
    }

    interface CommandLineWatch {
        void watchErrLine(String line);

        void watchInputLine(String line);
    }

    // 使用builder模式构建
    static class ShellCommandBuilder {
        private int timeout = 0;
        private Map<String, String> envs;
        private File workDir;
        private String[] commands;
        private String encoding = "UTF-8";
        private CommandLineWatch lineWatch;

        ShellCommandBuilder() {
        }

        public ShellCommandBuilder(CommandLineWatch lineWatch) {
            this.lineWatch = lineWatch;
        }

        ShellCommandBuilder setWorkDir(File workDir) {
            this.workDir = workDir;
            return this;
        }

        ShellCommandBuilder setCommands(String... cmd) {
            if (cmd == null) {
                throw new NullPointerException("cmd is null");
            }
            this.commands = cmd;
            return this;
        }

        ShellCommandBuilder setEncoding(String encoding) {
            this.encoding = (StringUtil.isEmpty(encoding) ? "UTF-8" : encoding);
            return this;
        }

        ShellCommandBuilder addEnv(String key, String value) {
            if (envs == null) envs = new HashMap<>();
            envs.put(key, value);
            return this;
        }

        ShellCommandBuilder removeEnc(String key) {
            if (envs != null) {
                envs.remove(key);
            }
            return this;
        }

        ShellCommand build() {
            return new ShellCommand(timeout, envs, workDir, encoding, commands, lineWatch);
        }
    }


}
